#导入工具包
import numpy as np
# import argparse
# import imutils
import cv2
import matplotlib as plt

# 正确答案,8道题，每题4个选项
ANSWER_KEY = {0: 1, 1: 3, 2: 0, 3: 1, 4: 2,
			  5:3, 6:2, 7:2}

#
# def read_and_convert_to_gray(image_path):
#     image = cv2.imread(image_path)
#     gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#     cv2.imshow("gray_image", gray_image)
#     cv2.waitKey(500)
#     return gray_image



def remove_noise(gray_image):
	kernel = np.ones((3, 3), np.uint8)
	opening = cv2.morphologyEx(gray_image, cv2.MORPH_OPEN, kernel, iterations=2)
	# 使用高斯模糊，去除噪音点
	blurred = cv2.GaussianBlur(opening, (5, 5), 0)
	cv2.imshow("GaussianBlur", blurred)
	cv2.waitKey(500)
	return blurred

def find_contours(opening):
	# 检测图像中明显的边缘，阈值最可以设为50-200，使用此参数进行明显的边缘检测
	edges = cv2.Canny(opening, 50, 150)
	cv2.imshow("edges", edges)
	cv2.waitKey(0)

	# 先查找图像的轮廓，再将找到的轮廓画出来。RETR_TREE
	contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL,
								   cv2.CHAIN_APPROX_SIMPLE)

	contours_img = opening.copy()  # 注意需要copy,原图不要改变
	# 以contours_img为背景将cnts中的所有轮廓以白色填充
	# 画全部轮廓
	res = cv2.drawContours(contours_img, contours, -1, (0, 0, 255), 2)
	cv2.imshow("contours_img", contours_img)
	cv2.waitKey(1000)
	return contours

def perspective_transform(image, contours):
	rect = cv2.minAreaRect(contours[0])
	box = cv2.boxPoints(rect)
	box = np.int0(box)
	width = int(rect[1][0])
	height = int(rect[1][1])
	src_pts = box.astype("float32")
	dst_pts = np.array([[0, height - 1],
						[0, 0],
						[width - 1, 0],
						[width - 1, height - 1]], dtype="float32")
	M = cv2.getPerspectiveTransform(src_pts, dst_pts)
	warped = cv2.warpPerspective(image, M, (width, height))
	return warped
# 按顺序找到对应坐标0123分别是 左上，右上，右下，左下
def order_points(pts):
	# 一共4个坐标点
	rect = np.zeros((4, 2), dtype = "float32")

	# 按顺序找到对应坐标0123分别是 左上，右上，右下，左下
	# 计算左上，右下
	s = pts.sum(axis = 1)
	rect[0] = pts[np.argmin(s)]
	rect[2] = pts[np.argmax(s)]

	# 计算右上和左下
	diff = np.diff(pts, axis = 1)
	rect[1] = pts[np.argmin(diff)]
	rect[3] = pts[np.argmax(diff)]

	return rect

def find_Answersheet(gray_image,contours):
	docCnt = None  # 记录答题卡的轮廓

	# 2、透视变换矫正目标区域，去除背景只留下答题区域部分
	if len(contours) > 0:
		# 根据轮廓大小进行排序
		contours = sorted(contours, key=cv2.contourArea, reverse=True)

		# 遍历每一个轮廓
		for c in contours:
			# 近似
			peri = cv2.arcLength(c, True)
			approx = cv2.approxPolyDP(c, 0.02 * peri, True)

			# 准备做透视变换
			if len(approx) == 4:
				docCnt = approx
				break
	return docCnt
# 透视变换
def four_point_transform(image, pts):
	# 获取输入坐标点
	rect = order_points(pts)
	(tl, tr, br, bl) = rect# 左上，右上，右下，左下

	# 计算输入的w和h值
	widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) #答题卡的下边长
	widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) #答题卡的上边长
	maxWidth = int((widthA+ widthB)/2)		#maxWidth:变换后的ROI矩形的宽度

	heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) #答题卡的右边长
	heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))#答题卡的左边长
	maxHeight = int((heightA+ heightB)/2)	#maxWidth:变换后的ROI矩形的高度

	# 变换后对应坐标位置
	dst = np.array([
		[0, 0],
		[maxWidth - 1, 0],
		[maxWidth - 1, maxHeight - 1],
		[0, maxHeight - 1]], dtype = "float32")

	# 计算变换矩阵
	#透视变换
	M = cv2.getPerspectiveTransform(rect,dst )
	warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
	cv2.imshow("four_point_transform", warped)
	cv2.waitKey(1000)
	# 返回变换后结果
	return warped

#以x坐标作为sorted函数排序关键字,对轮廓cnts从左到右排序
def sort_contours_x(cnts):
	# #遍历轮廓，并boundingRect()到包覆此轮廓的最小正矩形
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]

	# zip()将对象中对应的元素打包成一个个元组，然后返回由这些元组组成的列表。
	#sorted使用轮廓的最小正矩形坐标b[1][0]，对输入的轮廓cnts进行排序
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
                                        key=lambda b: b[1][0]))
    return cnts

#以轮廓的最小正矩形y坐标作为sorted函数排序关键字,对轮廓cnts从上到下进行排序
def sort_contours_y(cnts):

    reverse = False
	# 遍历轮廓，用boundingRect()得到包覆此轮廓的最小正矩形
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]

	# zip()将对象中对应的元素打包成一个个元组，然后返回由这些元组组成的列表。
	#sorted使用轮廓的最小正矩形y坐标b[1][1]，对输入的轮廓cnts进行排序
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
                                        key=lambda b: b[1][1]))
    return cnts


def check_options(warped ):
	# 变换为黑白图
	# thresh = cv2.Canny(warped, 50, 150)
	# cv2.imshow("Canny", thresh)
	# cv2.waitKey(500)

	# Otsu's 阈值处理 二值处理，变换为黑白图
	# ret, thresh = cv2.threshold(warped, 0, 255,
	#                 cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
	threst = cv2.adaptiveThreshold(
		warped, 255, cv2.ADAPTIVE_THREST_GAUSSION_C, cv2.THRESH_BINARY_INV, 25, 5
	)
	cv2.imshow("thresh", thresh)
	cv2.waitKey(500)

	# 从透视后的答题卡中查找轮廓， 遍历每一个轮廓(涂色选项）
	cnts, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL,
									   cv2.CHAIN_APPROX_SIMPLE)

	# 遍历所有轮廓，筛选出答案选项共计8*4=32个小长方形轮廓
	OptionCnts = []  # 答题卡的图像选项
	for c in cnts:
		# 计算比例和大小
		(x, y, w, h) = cv2.boundingRect(c)  # 得到包覆轮廓c的最小正矩形
		ar = w / float(h)

		# 根据实际情况指定一个矩形答题选项的大小和宽高比
		if w >= 30 and h >= 20 and ar >= 1.2 and ar <= 2:
			OptionCnts.append(c)

	print(len(OptionCnts))

	# 将检测到的轮廓按照从上到下进行排序，利用纵坐标。
	OptionCnts = sort_contours_y(OptionCnts)
	correctOption = 0  # 记录正确涂色的题目个数

	warped = cv2.cvtColor(warped, cv2.COLOR_GRAY2BGR)
	# 答案每排有4个选项
	# enumerate() 将一个列表组合为一个索引序列，同时列出数据和数据下标。
	# np.arange()返回一个列表，第一个参数为起点，第二个参数为终点，第三个参数为步长。
	# 这里就是将4作为一步，一排4个小长方形轮廓
	for (q, i) in enumerate(np.arange(0, len(OptionCnts), 4)):
		# 以x坐标作为sorted函数排序关键字,对一排5个轮廓从左到右排序
		cnts = sort_contours_x(OptionCnts[i:i + 4])
		bubbled = None

		# 遍历一道题目的4个选项涂色结果
		for (j, c) in enumerate(cnts):
			# 使用mask来判断结果
			mask = np.zeros(thresh.shape, dtype="uint8")
			cv2.drawContours(mask, [c], -1, 255, -1)  # -1表示填充
			cv2.imshow('mask', mask)
			cv2.waitKey(300)
			# 通过位与运算计算非零点数量来算是否选择这个答案
			mask = cv2.bitwise_and(thresh, thresh, mask=mask)
			total = cv2.countNonZero(mask)
			#print("bitwise:", thresh, mask, total)

			# 通过阈值判断
			if bubbled is None or total > bubbled[0]:
				bubbled = (total, j)

		# 对比正确答案
		color = (0, 0, 255)  # 用红色标记错误
		k = ANSWER_KEY[q]

		# 判断正确
		if k == bubbled[1]:
			color = (255, 255, 0)  # 用天蓝色标记正确答案
			correctOption += 1
		# 绘图
		cv2.drawContours(warped, [cnts[k]], -1, color, 4)
		cv2.imshow('warped',warped)
	return correctOption

def cv_show(name, img):
        cv2.imshow(name, img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()


def main():
	# 1.预处理
	# img = cv2.imread('images2\\test_08.png')

	image_path = "images/test2.jpg"
	image = cv2.imread(image_path)
	gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

	opening = remove_noise(gray_image)
	contours = find_contours(opening)

	#从轮廓中找到答题卡（轮廓中最大的矩形）
	docCnt = find_Answersheet(gray_image,contours)
	print(docCnt)
	# 执行透视变换
	# warped = perspective_transform(gray_image, contours)
	warped = four_point_transform(gray_image, docCnt.reshape(4, 2))


	# contours = threshold_and_find_contours(warped)
	# filled_options = count_non_zero_pixels(contours, warped)
	# score = compare_with_correct_answers(filled_options, correct_answers, threshold)
	correctOption = check_options(warped,)

	score = (correctOption / 8) * 100  # 8道题目
	print("score: {:.1f}".format(score))

	examImage = cv2.putText(image, "{:.2f}".format(score), (10, 30),
							cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1)
	# 显示评分后的试卷及考试成绩
	cv_show('Exam', examImage)

if __name__ == "__main__":
    main()
